#!/usr/bin/env node

// omg I am tired of code

import {getPlayer} from './players.js'
import {parseOptions} from './general-util.js'
import {getItemPathString} from './playlist-utils.js'
import Backend from './backend.js'
import setupClient from './client.js'
import TelnetServer from './telnet.js'

import {
  makeSocketServer,
  makeSocketClient,
  attachBackendToSocketClient,
  attachSocketServerToBackend,
} from './socket.js'

import {CommandLineInterface} from 'tui-lib/util/interfaces'
import * as ansi from 'tui-lib/util/ansi'

import {readFile, writeFile} from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'

// Hack to get around errors when piping many things to stdout/err
// (from general-util promisifyProcess)
process.stdout.setMaxListeners(Infinity)
process.stderr.setMaxListeners(Infinity)

process.on('unhandledRejection', error => {
  console.error(ansi.setForeground(ansi.C_RED) + "** There was an uncatched error! **" + ansi.resetAttributes())
  console.error("Don't worry, your music files are all okay.")
  console.error("This just means there was a bug in mtui.")
  console.error("In order to verify that the program won't run weirdly, it has stopped.")
  console.error(ansi.setForeground(ansi.C_RED) + "Error stack:" + ansi.resetAttributes())
  console.error(error.stack)
  console.error(ansi.setForeground(ansi.C_RED) + "Error object:" + ansi.resetAttributes())
  console.error(error)
  console.error("(End of error log.)")
  process.stdout.write(ansi.cleanCursor())
  process.exit(1)
})

async function main() {
  const playlistSources = []

  const options = await parseOptions(process.argv.slice(2), {
    'player': {
      type: 'value',
      async validate(playerName) {
        if (await getPlayer(playerName)) {
          return true
        } else {
          return 'a known player identifier'
        }
      }
    },

    'player-options': {type: 'series'},
    'stress-test': {type: 'flag'},
    'socket-client': {type: 'value'},
    'socket-name': {type: 'value'},
    'socket-server': {type: 'value'},
    'telnet-server': {type: 'flag'},
    'skip-config-file': {type: 'flag'},
    'config-file': {type: 'value'},

    [parseOptions.handleDashless](option) {
      playlistSources.push(option)
    },
  })

  if (options['player-options'] && !options['player']) {
    console.error('--player must be specified in order to use --player-options')
    process.exit(1)
  }

  let jsonConfig = {}
  let jsonError = null

  const jsonPath =
    (options['config-file']
      ? path.resolve(options['config-file'])
      : path.join(os.homedir(), '.mtui', 'config.json'))

  try {
    jsonConfig = JSON.parse(await readFile(jsonPath))
  } catch (error) {
    if (error.code !== 'ENOENT') {
      jsonError = error
    }
  }

  if (jsonError) {
    console.error(`Error loading JSON config:`)
    console.error(jsonError.message)
    console.error(`Edit the file below to fix the error, or run mtui with --skip-config-file.`)
    console.error(jsonPath)
    process.exit(1)
  }

  const backendConfig =
    (options['socket-server']
      ? {
          playerName: 'ghost',
        }
      : {
          playerName: options['player'],
          playerOptions: options['player-options'],
        })

  const appConfig =
    (options['socket-server']
      ? {
          showPartyControls: true,
          canControlPlayback: false,
          canControlQueue: false,
          canControlQueuePlayers: false,
          canProcessMetadata: false,
        }
   : options['socket-client']
      ? {
          showPartyControls: true,
        }
      : {})

  const backend = new Backend(backendConfig)

  const setupResult = await backend.setup()
  if (setupResult.error) {
    console.error(setupResult.error)
    process.exit(1)
  }

  if (options['socket-server']) {
    const socketServer = makeSocketServer()
    attachSocketServerToBackend(socketServer, backend)
    socketServer.listen(options['socket-server'])

    const socketClient = makeSocketClient()
    attachBackendToSocketClient(backend, socketClient)
    socketClient.socket.connect(options['socket-server'])

    backend.setPartyNickname('Internal Client')
    backend.announceJoinParty()
  }

  if (!options['socket-server']) {
    backend.on('playing', track => {
      if (track) {
        writeFile(backend.rootDirectory + '/current-track.txt',
          getItemPathString(track))
        writeFile(backend.rootDirectory + '/current-track.json',
          JSON.stringify(track, null, 2))
      }
    })
  }

  const { appElement, dirtyTerminal, flushable, root } = await setupClient({
    backend,
    screenInterface: new CommandLineInterface(),
    writable: process.stdout,
    appConfig,
  })

  appElement.on('quitRequested', () => {
    if (telnetServer) {
      telnetServer.disconnectAllSockets('User closed mtui - see you!')
    }
    process.exit(0)
  })

  appElement.on('suspendRequested', () => {
    process.kill(process.pid, 'SIGTSTP')
  })

  process.on('SIGCONT', () => {
    flushable.resizeScreen({lines: flushable.screenLines, cols: flushable.screenCols})
    process.stdin.setRawMode(false)
    process.stdin.setRawMode(true)
    dirtyTerminal()
    root.renderNow()
  })

  if (!options['socket-server'] && playlistSources.length === 0) {
    if (jsonConfig.defaultPlaylists) {
      playlistSources.push(...jsonConfig.defaultPlaylists)
    } else {
      playlistSources.push({
        name: 'My ~/Music Library',
        comment: (
          '(Add tracks and folders to ~/Music to make them show up here,' +
          ' or pass mtui your own playlist.json file!)'),
        source: ['crawl-local', os.homedir() + '/Music']
      })
    }
  }

  const loadPlaylists = async () => {
    for (const source of playlistSources) {
      await appElement.loadPlaylistOrSource(source, true)
    }
  }

  const loadPlaylistPromise = loadPlaylists()

  let telnetServer
  if (options['telnet-server']) {
    telnetServer = new TelnetServer(backend)
    await telnetServer.listen(1244)
    appElement.attachAsServerHost(telnetServer)
  }

  if (options['socket-client']) {
    const socketClient = makeSocketClient()
    const [ p1, p2 ] = options['socket-client'].split(':')
    const host = p2 && p1
    const port = p2 ? p2 : p1
    socketClient.socket.connect(port, host)

    attachBackendToSocketClient(backend, socketClient)

    let nickname = process.env.USER
    if (options['socket-name']) {
      nickname = options['socket-name']
    }
    backend.setPartyNickname(nickname)
    backend.announceJoinParty()
  }

  if (options['stress-test']) {
    await loadPlaylistPromise

    const w = 80
    const h = 40
    flushable.resizeScreen({lines: w, cols: h})
    root.w = w
    root.h = h
    root.fixAllLayout()

    /* eslint-disable-next-line no-unused-vars */
    const XXstress = func => '[disabled]'

    const stress = func => {
      const start = Date.now()
      let n = 0
      while (Date.now() < start + 1000) {
        func()
        n++
      }
      return n
    }

    const nRenderAndFlush = stress(() => {
      root.renderTo(flushable)
      flushable.flush()
    })

    const nFixAllLayout = stress(() => {
      root.fixAllLayout()
    })

    const listings = appElement.tabber.tabberElements
    const lastListing = listings[listings.length - 1]
    const nBuildItems = stress(() => {
      lastListing.buildItems()
    })

    process.stdout.write(ansi.cleanCursor() + ansi.clearScreen() + '\n')
    console.log('# of times we can render & flush:', nRenderAndFlush)
    console.log('# of times we can fix all layout:', nFixAllLayout)
    console.log('# of times we can build items:', nBuildItems)

    process.exit(0)

    return
  }
}

main().catch(err => {
  console.error(err)
  process.exit(1)
})
